package com.vision.sales.sv.serv;

import java.io.IOException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import net.sf.jasperreports.engine.JRDataSource;
import net.sf.jasperreports.engine.data.JRBeanCollectionDataSource;

import org.hibernate.criterion.Restrictions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;

import ar.com.fdvs.dj.domain.DJCalculation;

import com.vision.core.cm.exception.RpcException;
import com.vision.core.cm.util.DateUtils;
import com.vision.core.sv.db.CustomSessionFactoryBean;
import com.vision.core.sv.db.mgr.IDataManager;
import com.vision.core.sv.db.mgr.ISimpleCriteria;
import com.vision.core.sv.db.mgr.SimpleCriteria;
import com.vision.core.sv.serv.util.ServletUtils;
import com.vision.core.sv.util.CollectionUtils;
import com.vision.sales.cm.data.CollectionStatus;
import com.vision.sales.cm.data.SalesPermissions;
import com.vision.sales.cm.data.TransactionType;
import com.vision.sales.cm.db.data.Customer;
import com.vision.sales.cm.db.data.SalesInvoice;
import com.vision.sales.cm.db.data.TransactionEntry;
import com.vision.sales.sv.db.mgr.SalesInvoiceManager;
import com.vision.sales.sv.db.mgr.TransactionEntryManager;
import com.vision.sales.sv.report.Report;
import com.vision.sales.sv.report.ReportWriter;

/**
 * Servlet for printing Customer Ledger report.
 * 
 * This servlet is accessible via the /print/customer_ledger path in the URL (see web.xml)
 * 
 * GET parameters:
 * <ul>
 * 	<li>title - the title that will appear in the report
 * 	<li>span - start and end date covered. Format: yyyyMMdd-yyyyMMdd</li>
 * 	<li>companies - optional comma-separated list of company ids indicating which companies to include. If not specified, all companies will be included.</li>
 * 	<li>customers - optional comma-separated list of customer ids indicating which customers to include. If not specified, all customers will be included.</li>
 * 	<li>types - optional comma-separated list of integers indicating which transaction types to include. If not specified, all types will be included.</li>
 * 	<li>showzeros - optional boolean value to indicate if transactions with zero amounts are to be included. Defaults to true.</li>
 * </ul>
 * 
 * Example:
 * <pre>/print/customer_ledger?title=Customer%20Ledger&span=20120101-20121231&companies=1,2&customers=5,10,22&types=0,1,2&showzeros=true</pre>
 * 
 * @author Mark
 */
@Service("print_customer_ledger")
public class PrintCustomerLedgerServlet extends HttpServlet {
	
	private static final long serialVersionUID = 1L;
	
	private static final Logger log = LoggerFactory.getLogger(PrintCustomerLedgerServlet.class);
	
	@Autowired
	@Qualifier("transactionEntryManager")
	private IDataManager<TransactionEntry> transactionEntryManager;
	
	@Autowired
	@Qualifier("salesInvoiceManager")
	private IDataManager<SalesInvoice> invoiceManager;
	
	public PrintCustomerLedgerServlet() {
		log.debug("Created " + getClass().getName());
	}
	
	@Override
	protected void doGet(HttpServletRequest req, HttpServletResponse resp)
			throws ServletException, IOException {
		try {
			ServletUtils.checkPermissions(SalesPermissions.ViewCustomerLedgerReport);
		} catch (RpcException e) {
			resp.sendError(HttpServletResponse.SC_FORBIDDEN, "You are not authorized to perform this operation.");
			return;
		} 
		
		// Parameters --
		log.debug("Parsing parameters from URL...");
		CustomerLedgerParams params = new CustomerLedgerParams(req);
		if (params.error != null) {
			resp.sendError(HttpServletResponse.SC_BAD_REQUEST, params.error);
			return;
		}
		
		// XXX @Autowired doesn't seem to work, thus this patch
		transactionEntryManager = new TransactionEntryManager();
		((TransactionEntryManager) transactionEntryManager).setSessionFactory(CustomSessionFactoryBean.getFactory());
		
		invoiceManager = new SalesInvoiceManager();
		((SalesInvoiceManager) invoiceManager).setSessionFactory(CustomSessionFactoryBean.getFactory());
		
		try {
			DateFormat format = new SimpleDateFormat("MMM d, yyyy");
			
			// Build the Report
			log.debug("Building report...");
			Report report = new Report();
			report.setFilename("Customer Ledger Report " + params.span);
			report.setSize(params.size);
			report.setOrientation(params.orientation);
			report.setTitle(params.title);
			report.setSubtitle(format.format(params.start) + " - " + format.format(params.end));

			// Columns and Grouping
			report.addStringColumn("customer.code", "Customer", 10, null);
			report.addDateColumn("date", "Date", 20, "MM/dd/yyyy");
			report.addStringColumn("type.displayText", "Transaction", 40, null);
			report.addStringColumn("transactionRefNo", "Ref. No.", 40, null);
			report.addDoubleColumn("debit", "Debit", 25, DJCalculation.SUM);
			report.addDoubleColumn("credit", "Credit", 25, DJCalculation.SUM);
			report.addDoubleColumn("balance", "Balance", 25);
			report.addGroup("customer.code");
			report.addGroupFooter("customer.code", "debit", DJCalculation.SUM);
			report.addGroupFooter("customer.code", "credit", DJCalculation.SUM);
			
			// Data Source
			log.debug("Creating data source...");
			report.setDataSource(getDataSource(params));
			
			// Print and Write to Stream
			log.debug("Writing report to stream...");
			ReportWriter.write(report, resp);
		} catch (Exception e) {
			log.error(e.getMessage(), e);
			resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "The server was unable to process the request.");
		}
	}
	
	private JRDataSource getDataSource(CustomerLedgerParams params) {
		List<TransactionEntry> entries = getTransactionEntries(params);
		// Add the starting balances per customer
		entries.addAll(getStartingBalances(params));
		
		// Sort by Customer name (since we can't sort it by customer name via SQL)
		Collections.sort(entries, new Comparator<TransactionEntry>() {
			@Override
			public int compare(TransactionEntry arg0, TransactionEntry arg1) {
				Customer c0 = arg0.getCustomer();
				Customer c1 = arg1.getCustomer();
				int diff = c0.getName().compareToIgnoreCase(c1.getName());
				if (diff != 0)
					return diff;
				return arg0.compareTo(arg1);
			}
		});
		
		boolean hasSalesInvoice = CollectionUtils.containsAny(entries, "getType", TransactionType.SALES_INVOICE);
		Map<String, SalesInvoice> invoicesMap = new HashMap<String, SalesInvoice>();
		if (hasSalesInvoice) {
			// FIXME 0 Instead, get all the ids of the sales invoices from 'entries' and use it to get
			// the sales invoices.
			invoicesMap = getSalesInvoices(params);
		}
		
		double balance = 0; // The running balance (for each customer) 
		Long customerId = null;
		
		for (TransactionEntry entry : entries) {
			Customer customer = entry.getCustomer();
			TransactionType type = entry.getType();
			
			// Reset for each new customer
			if (customerId == null || !customerId.equals(customer.getId())) {
				balance = 0;
				customerId = customer.getId();
			}
			
			if (type == TransactionType.STARTING_BALANCE) {
				balance = entry.getBalance();
				entry.setBalance(balance);
			} else {
				balance += (entry.getDebit() - entry.getCredit());
				entry.setBalance(balance);
				
				if (entry.getType() == TransactionType.SALES_INVOICE) {
					String refNo = entry.getTransactionRefNo();
					SalesInvoice invoice = invoicesMap.get(entry.getTransactionRefNo());
					if (invoice == null)
						continue;
					
					if (invoice.getCollectionStatus() == CollectionStatus.FULLY_PAID) {
						refNo += " x";
					} else if (invoice.getCollectionStatus() == CollectionStatus.PARTIALLY_PAID) {
						refNo += " *";
					}
					
					entry.setTransactionRefNo(refNo);
				}
			}
		}
		
		JRBeanCollectionDataSource ds = new JRBeanCollectionDataSource(entries);
		return ds;
	}
	
	@SuppressWarnings("unchecked")
	private List<TransactionEntry> getPreviousTransactionEntries(CustomerLedgerParams params) {
		ISimpleCriteria crit = SimpleCriteria.forClass(TransactionEntry.class);
		crit.lt("date", params.start);
		
		if (params.companyIds != null && !params.companyIds.isEmpty())
			crit.in("company.id", params.companyIds);
		
		if (params.customerIds != null && !params.customerIds.isEmpty())
			crit.in("customer.id", params.customerIds);
		
		if (params.types != null && !params.types.isEmpty())
			crit.in("type", params.types);
		
		crit.asc("date", "customer.id", "type");
		
		return transactionEntryManager.getTemplate().findByCriteria(crit.getDetached());
	}
	
	/**
	 * Returns a "Starting Balance" entry for each customer.
	 * 
	 * @param params
	 * @return
	 */
	private List<TransactionEntry> getStartingBalances(CustomerLedgerParams params) {
		List<TransactionEntry> entries = getPreviousTransactionEntries(params);
		
		Map<Long, TransactionEntry> map = new HashMap<Long, TransactionEntry>();
		
		for (TransactionEntry entry : entries) {
			// Get the "Starting Balance" entry for the customer
			TransactionEntry startingBalance = map.get(entry.getCustomer().getId());
			if (startingBalance == null) {
				startingBalance = new TransactionEntry();
				startingBalance.setCompany(entry.getCompany()); // Not really needed
				startingBalance.setCustomer(entry.getCustomer());
				startingBalance.setDate(new Date(params.start.getTime() - DateUtils.ONE_DAY_IN_MILLIS)); // Less one day
				startingBalance.setDebit(0d);
				startingBalance.setCredit(0d);
				startingBalance.setType(TransactionType.STARTING_BALANCE);
				startingBalance.setTransactionRefNo(""); // Not really needed, so we just set to empty
				map.put(entry.getCustomer().getId(), startingBalance);
			}
			
			startingBalance.setBalance(startingBalance.getBalance() + entry.getDebit() - entry.getCredit());
		}
		
		return new ArrayList<TransactionEntry>(map.values());
	}
	
	@SuppressWarnings("unchecked")
	private List<TransactionEntry> getTransactionEntries(CustomerLedgerParams params) {
		ISimpleCriteria crit = SimpleCriteria.forClass(TransactionEntry.class);
		crit.between("date", params.start, params.end);
		
		if (params.companyIds != null && !params.companyIds.isEmpty())
			crit.in("company.id", params.companyIds);
		
		if (params.customerIds != null && !params.customerIds.isEmpty())
			crit.in("customer.id", params.customerIds);
		
		if (params.types != null && !params.types.isEmpty())
			crit.in("type", params.types);
		
		if (!params.showZeros) {
			crit.add( Restrictions.or(Restrictions.gt("credit", 0d), Restrictions.gt("debit", 0d)) );
		}
		
		crit.asc("date", "type");
		
		return transactionEntryManager.getTemplate().findByCriteria(crit.getDetached());
	}
	
	@SuppressWarnings("unchecked")
	private Map<String, SalesInvoice> getSalesInvoices(CustomerLedgerParams params) {
		ISimpleCriteria crit = SimpleCriteria.forClass(SalesInvoice.class);
		
		if (params.companyIds != null && !params.companyIds.isEmpty())
			crit.in("company.id", params.companyIds);
		
		if (params.customerIds != null && !params.customerIds.isEmpty())
			crit.in("customer.id", params.customerIds);
		
		List<SalesInvoice> list = invoiceManager.getTemplate().findByCriteria(crit.getDetached());
		return CollectionUtils.mapBy(list, "getGroupAndNo");
	}
	
	// Inner classes %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
	
	static class CustomerLedgerParams extends PeriodSalesReportParams {
		
		String span;
		Date start;
		Date end;
		List<TransactionType> types;
		boolean showZeros = true;
		
		public CustomerLedgerParams(HttpServletRequest req) {
			super(req);
			
			setDefaultTitle("Customer Ledger");
			
			span = req.getParameter("span");
			try {
				String[] split = span.split("-");
				start = yyyyMMdd.parse(split[0]);
				end = yyyyMMdd.parse(split[1]);
			} catch (Exception e) {
				error = "Unable to parse start and end dates.";
			}
			
			String sTypes = req.getParameter("types");
			if (sTypes != null) {
				try {
					String[] split = sTypes.split(",");
					types = new ArrayList<TransactionType>();
					for (String s : split) {
						types.add(TransactionType.values()[Integer.parseInt(s)]);
					}
				} catch (Exception e) {
				}
			}
			
			String showZeroes = req.getParameter("showzeroes");
			if (showZeroes != null) {
				try {
					showZeros = Boolean.parseBoolean(showZeroes);
				} catch (Exception e) {
				}
			}
		}
		
	}

}
